本节代码对应 GitHub 分支: chapter8

仓库传送门 (opens new window)

进度条组件是播放器中至关重要的组件,我们单独抽出来讲。

# 环形进度条组件

环形主要用于迷你播放器上,简单地运用 svg 来进行实现。

//baseUI/progress-circle.js
import React from 'react';
import styled from'styled-components';
import style from '../../assets/global-style';

const CircleWrapper = styled.div`
  position: relative;
  circle {
    stroke-width: 8px;
    transform-origin: center;
    &.progress-background {
      transform: scale (0.9);
      stroke: ${style ["theme-color-shadow"]};
    }
    &.progress-bar {
      transform: scale (0.9) rotate (-90deg);
      stroke: ${style ["theme-color"]};
    }
  }
`

function ProgressCircle (props) {
  const {radius, percent} = props;
  // 整个背景的周长
  const dashArray = Math.PI * 100;
  // 没有高亮的部分,剩下高亮的就是进度
  const dashOffset = (1 - percent) * dashArray;

  return (
    <CircleWrapper>
      <svg width={radius} height={radius} viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg">
        <circle className="progress-background" r="50" cx="50" cy="50" fill="transparent"/>
        <circle className="progress-bar" r="50" cx="50" cy="50" fill="transparent"
                strokeDasharray={dashArray}
                strokeDashoffset={dashOffset}/>
      </svg>
      {props.children}
    </CircleWrapper>
  )
}

export default React.memo (ProgressCircle);

现在来把它应用到 mini 播放器中:

// 先 mock 一份 percent 数据
let percent = 0.2;

// 将原来的暂停按钮部分修改
<div className="control">
  <ProgressCircle radius={32} percent={percent}>
    <i className="icon-mini iconfont icon-pause">&#xe650;</i>
  </ProgressCircle>
</div>

现在 20% 的效果就出现了。

image-20210215203338330

# 线形进度条组件

现在我们来构建 normalPlayer 中的线性进度条,相信这也是大家会比较常用的一个组件。大家可能在平时的开发中,直接用的 UI 框架来完成,但你懂得背后是如何实现的吗? 现在就趁这个机会来试一试吧。

由于涉及到比较复杂的交互,我们这里做重点来讲解。

# UI 构建

首先构建 UI:

//baseUI/progressBar/index.js
import React, {useEffect, useRef, useState } from 'react';
import styled from'styled-components';
import style from '../../assets/global-style';
import { prefixStyle } from './../../api/utils';

const ProgressBarWrapper = styled.div`
  height: 30px;
  .bar-inner {
    position: relative;
    top: 13px;
    height: 4px;
    background: rgba (0, 0, 0, .3);
    .progress {
      position: absolute;
      height: 100%;
      background: ${style ["theme-color"]};
    }
    .progress-btn-wrapper {
      position: absolute;
      left: -15px;
      top: -13px;
      width: 30px;
      height: 30px;
      .progress-btn {
        position: relative;
        top: 7px;
        left: 7px;
        box-sizing: border-box;
        width: 16px;
        height: 16px;
        border: 3px solid ${style ["border-color"]};
        border-radius: 50%;
        background: ${style ["theme-color"]};
      }
    }
  }
`

function ProgressBar (props) {
  return (
    <ProgressBarWrapper>
      <div className="bar-inner">
        <div className="progress"></div>
        <div className="progress-btn-wrapper">
          <div className="progress-btn"></div>
        </div>
      </div>
    </ProgressBarWrapper>
  )
}

为了能及时看到效果,我们在 normalPlayer 中来引入这个组件。

import ProgressBar from "../../../baseUI/progress-bar/index";

<ProgressWrapper>
  <span className="time time-l">0:00</span>
  <div className="progress-bar-wrapper">
    <ProgressBar percent={0.2}></ProgressBar>
  </div>
  <div className="time time-r">4:17</div>
</ProgressWrapper>

ProgressWrapper 样式组件已经实现,现在只需从 style.js 引入即可。

现在,就可以看到基本的进度条的样子了。

image-20210215203403598

# 进度条交互逻辑开发

首先,进度条组件作为播放器的一部分,我们思考一下将它被拆分出去后的功能,一方面是要响应用户的拖动或点击动作,让进度条得以长度变化,另一方面是要执行播放器组件传递过来的进度改变时需要的回调。

好,我们先完成第一步。

// 即将使用的 hooks
import React, {useEffect, useRef, useState } from 'react';

const progressBar = useRef ();
const progress = useRef ();
const progressBtn = useRef ();
const [touch, setTouch] = useState ({});

const progressBtnWidth = 16;

//JSX 部分
<ProgressBarWrapper>
  <div className="bar-inner" ref={progressBar} >
    <div className="progress" ref={progress}></div>
    <div className="progress-btn-wrapper" ref={progressBtn}
        onTouchStart={progressTouchStart}
        onTouchMove={progressTouchMove}
        onTouchEnd={progressTouchEnd}
    >
      <div className="progress-btn"></div>
    </div>
  </div>
</ProgressBarWrapper>

现在来处理滑动事件的逻辑:

// 处理进度条的偏移
const _offset = (offsetWidth) => {
  progress.current.style.width = `${offsetWidth} px`;
  progressBtn.current.style.transform = `translate3d (${offsetWidth} px, 0, 0)`;
}

const progressTouchStart = (e) => {
  const startTouch = {};
  startTouch.initiated = true;//initial 为 true 表示滑动动作开始了
  startTouch.startX = e.touches [0].pageX;// 滑动开始时横向坐标
  startTouch.left = progress.current.clientWidth;// 当前 progress 长度
  setTouch (startTouch);
}

const progressTouchMove = (e) => {
  if (!touch.initiated) return;
  // 滑动距离
  const deltaX = e.touches [0].pageX - touch.startX;
  const barWidth = progressBar.current.clientWidth - progressBtnWidth;
  const offsetWidth = Math.min (Math.max (0, touch.left + deltaX), barWidth);
  _offset (offsetWidth);
}

const progressTouchEnd = (e) => {
  const endTouch = JSON.parse (JSON.stringify (touch));
  endTouch.initiated = false;
  setTouch (endTouch);
}

现在进度条就可以自由地拖动啦!

不过还有一种情况,就是用户点击进度条的时候,进度条也应该做相应的改变。

其实很简单,绑定点击事件即可。

<div className="bar-inner" ref={progressBar} onClick={progressClick}>
const progressClick = (e) => {
  const rect = progressBar.current.getBoundingClientRect ();
  const offsetWidth = e.pageX - rect.left;
  _offset (offsetWidth);
};

现在我们就完成了第一步啦,接下来当进度改变后,我们需要执行父组件传过来的回调函数。

// 取出回调函数
const {percentChange} = props;

const _changePercent = () => {
  const barWidth = progressBar.current.clientWidth - progressBtnWidth;
  const curPercent = progress.current.clientWidth/barWidth;// 新的进度计算
  percentChange (curPercent);// 把新的进度传给回调函数并执行
}

// 滑动完成时
const progressTouchEnd = (e) => {
  //...
  _changePercent ();
}
// 点击后
const progressClick = (e) => {
  //...
  _changePercent ();
}

由于 percentChange 的具体逻辑在父组件完成,与目前组件无关 至此,进度条组件就开发完成了。

阅读全文